﻿using System;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

namespace AMEE.MVVMBase
{
    /// <summary>
    /// The view Model base class.<para>
    /// Provides PropertyChanged Events, bindable to WPF-View.</para>
    /// </summary>
    [Serializable]
    public abstract class ViewModelBase : INotifyPropertyChanged, IDisposable
    {
        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Occurs when the initialization is completed.
        /// </summary>
        public event AsyncCompletedEventHandler InitializationCompleted;

        /// <summary>
        /// Initializes a new instance of the <see cref="ViewModelBase"/> class.
        /// </summary>
        /// <remarks>Calls the protected Initialize method as asynchronous Task</remarks>
        protected ViewModelBase() {
            var initializationTask = new Task(() => Initialize());
            initializationTask.ContinueWith(result => InitializationCompletedCallback(result));
            initializationTask.Start();
        }


        #region IDisposable Members

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose() {
            this.OnDispose();
        }

        /// <summary>
        /// Override this method to handle a call of Dispose.
        /// </summary>
        protected virtual void OnDispose() {
        }

        #endregion



        #region protected Methods

        /// <summary>
        /// Fires PropertyChange Event. You might override this method, if you want additional handling.
        /// </summary>
        /// <param name="propertyName"></param>
        protected virtual void OnPropertyChanged(String propertyName) {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        /// <summary>
        /// Initializes this instance.<para>
        /// Called by constructor. Override, to perform initialization tasks. See also InitializationCompleted event.</para>
        /// </summary>
        protected virtual void Initialize() {
        }

        #endregion

        #region private Helpers

        /// <summary>
        /// Callback method for the async initialization.
        /// </summary>
        /// <param name="result">The result.</param>
        private void InitializationCompletedCallback(IAsyncResult result) {
            var initializationCompleted = InitializationCompleted;
            if (initializationCompleted != null) {
                InitializationCompleted(this, new AsyncCompletedEventArgs(null, !result.IsCompleted, result.AsyncState));
            }
            InitializationCompleted = null;
        }

        #endregion

    }



    /// <summary>
    /// The view Model base class with reference to underling, changeable object (model).<para>
    /// Provides PropertyChanged Events, bindable to WPF-View.</para>
    /// </summary>
    public abstract class ViewModelBase<TModel> : ViewModelBase where TModel : class
    {
        private TModel model;

        /// <summary>
        /// The Model encapsulated by this ViewModel.
        /// </summary>
        /// <remarks>If you change this, all needed PropertyChanged events will be raised automatically.</remarks>
        [Browsable(false)]
        [Bindable(false)]
        public TModel Model {
            get {
                return model;
            }
            set {
                if (Model != value) {
                    // get all properties
                    var properties = this.GetType().GetProperties(BindingFlags.Public);
                    // all values before the model has changed
                    var oldValues = properties.Select(p => p.GetValue(this, null));
                    var enumerator = oldValues.GetEnumerator();

                    model = value;

                    // call OnPropertyChanged for all changed properties
                    foreach (var property in properties) {
                        enumerator.MoveNext();
                        var oldValue = enumerator.Current;
                        var newValue = property.GetValue(this, null);

                        if ((oldValue == null && newValue != null)
                            || (oldValue != null && newValue == null)
                            || (!oldValue.Equals(newValue))) {
                            OnPropertyChanged(property.Name);
                        }
                    }
                }
            }
        }


        /// <summary>
        /// Initializes a new instance of the <see cref="ViewModelBase"/> class.
        /// </summary>
        /// <remarks></remarks>
        protected ViewModelBase(TModel model)
            : base() {
            this.Model = model;
        }

        /// <summary>
        /// Returns a hash code for this instance.
        /// </summary>
        /// <returns>
        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. 
        /// </returns>
        public override int GetHashCode() {
            return Model.GetHashCode();
        }

        /// <summary>
        /// Determines whether the specified <see cref="System.Object"/> is equal to this instance.
        /// </summary>
        /// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param>
        /// <returns>
        ///   <c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>.
        /// </returns>
        public override bool Equals(object obj) {
            if (obj == null)
                return false;

            var other = obj as ViewModelBase<TModel>;

            if (other == null)
                return false;

            return Equals(other);
        }


        /// <summary>
        /// Determines whether the specified <see cref="ViewModelBase&lt;TModel&gt;"/> is equal to this instance.
        /// </summary>
        /// <param name="other">The <see cref="ViewModelBase&lt;TModel&gt;"/> to compare with this instance.</param>
        /// <returns>
        ///   <c>true</c> if the specified <see cref="ViewModelBase&lt;TModel&gt;"/> is equal to this instance; otherwise, <c>false</c>.
        /// </returns>
        public bool Equals(ViewModelBase<TModel> other) {
            if (other == null)
                return false;

            if (Model == null)
                return Model == other.Model;

            return Model.Equals(other.Model);
        }
    }
}
